-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
10-6 [BE] [검색리스트] pagination 범위 초과시 error 처리 #63
Conversation
this.rankingService.insertRedis(keyword); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테스트도 추가해주시면 좋겠네요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
정말 꼼꼼하게 테스트 코드를 작성해주신것 같아요. 외부 모듈 mock 작업도 여러모로 신경쓰신게 느껴졌어요. 존경합니다👍
describe('SearchController', () => { | ||
let controller: SearchController; | ||
let app: INestApplication; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let app: INestApplication; | |
let service: SearchService; | |
let app: INestApplication; |
상단에 service를 선언해서 각 테스트케이스들이 service 내부 메서드들을 테스트에 활용할 수 있게하면 좋을 것 같습니다.
{ | ||
provide: ElasticsearchService, | ||
useValue: elasticService, | ||
}, | ||
], | ||
}).compile(); | ||
controller = module.get(SearchController); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
controller = module.get(SearchController); | |
controller = module.get(SearchController); | |
service = module.get(SearchService); |
그러면 service module의 정의부가 필요해집니다.
// Case 1. elasticsearch에 data가 없을 경우 | ||
const keyword = 'coffee'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Case 1. elasticsearch에 data가 없을 경우 | |
const keyword = 'coffee'; | |
const spyGetElasticSearch = jest.spyOn(service, 'getElasticSearch'); | |
const spyGetCrossRefData = jest.spyOn(service, 'getCrossRefData'); | |
const keyword = 'coffee'; | |
// Case 1. elasticsearch에 data가 없을 경우 |
jest.spyOn을 사용하면 service의 내부 함수를 mock으로 대체하지 않아도 호출여부, 호출 방식 등을 알아낼 수 있다고 합니다.
두가지 케이스에서 searchService.getElasticSearch
, searchService.getCrossRefData
의 호출 여부를 테스트에 활용하면 분리된 케이스 검증이 가능할 것 같아요.
그리고 keyword 변수는 두 케이스에서 공통으로 사용하므로 같이 주석 상단으로 옮기면 좋을 것 같습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오~ 그렇네요. 서비스에 spyOn을 붙여서 함수가 잘 호출되고 있는지 확인하도록 할게요.
// Case 2. elasticsearch에 data가 있는 경우 | ||
const itemsByElasticsearch = await controller.getAutoCompletePapers({ keyword }); | ||
expect(itemsByElasticsearch.length).toBe(5); | ||
itemsByElasticsearch.forEach((item) => { | ||
expect(item instanceof PaperInfo).toBe(true); | ||
}); | ||
}); | ||
it('keyword 미포함시 error - GET /search/auto-complete?keyword=', () => { | ||
const url = (keyword: string) => `/search/auto-complete?keyword=${keyword}`; | ||
return request(app.getHttpServer()).get(url('')).expect(400); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mockElasticService()
함수에서 mockResolvedValueOnce로 elasticService.search의 목업 케이스 분리를 잘 해주셔서, 실제 테스트 코드내에서도 두 케이스가 다르게 동작하고 있는것을 확인했습니다.
그런데 현재의 코드는 두 케이스의 검증로직이 동일해서, 두 케이스가 실제로 구분되어 실행되고 있는지 확인하는 로직도 있으면 더 좋을 것 같아요.
// Case 2. elasticsearch에 data가 있는 경우 | |
const itemsByElasticsearch = await controller.getAutoCompletePapers({ keyword }); | |
expect(itemsByElasticsearch.length).toBe(5); | |
itemsByElasticsearch.forEach((item) => { | |
expect(item instanceof PaperInfo).toBe(true); | |
}); | |
}); | |
it('keyword 미포함시 error - GET /search/auto-complete?keyword=', () => { | |
const url = (keyword: string) => `/search/auto-complete?keyword=${keyword}`; | |
return request(app.getHttpServer()).get(url('')).expect(400); | |
}); | |
expect(spyGetElasticSearch).toBeCalledTimes(1); | |
expect(spyGetCrossRefData).toBeCalledTimes(1); | |
// Case 2. elasticsearch에 data가 있는 경우 | |
const itemsByElasticsearch = await controller.getAutoCompletePapers({ keyword }); | |
expect(itemsByElasticsearch.length).toBe(5); | |
itemsByElasticsearch.forEach((item) => { | |
expect(item instanceof PaperInfo).toBe(true); | |
}); | |
expect(spyGetElasticSearch).toBeCalledTimes(2); | |
expect(spyGetCrossRefData).toBeCalledTimes(1); |
case1: spyGetElasticSearch, spyGetCrossRefData 모두 호출
case2: spyGetElasticSearch만 호출
이렇게 검증하면 되지 않을까요?
}); | ||
search.mockResolvedValue({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mockResolvedValueOnce로 호출 횟수에따라 케이스를 구분해서 테스트에 활용할 수 있었군요! 좋은 방법을 찾아서 적용하신 것 같아요. 배워갑니다.
}); | |
search.mockResolvedValue({ | |
}) | |
.mockResolvedValue({ |
이부분 체이닝이 가능하더라고요. jest 공식 docs 예제에서도 저 메서드를 사용할 때 체이닝 패턴을 주로 사용 하던데, 개인적으로는 체이닝을 하면 이런 순서로 호출될것이다~ 라는 느낌을 주는 것 같습니다..!?!! (물론 순서가 바뀌더라도 mockResolvedValueOnce가 항상 먼저 불리긴합니다..) 사소한부분이라... 성빈님의 취향에 맡기겠습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
체이닝 되는 게 가독성이 더 좋은 것 같습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
확인했습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
테스트코드를 정말 꼼꼼하게 작성해 주셨네요 👍 테스트 이름도 알기 쉽게 작성되어 있어서 이해가 쉬웠습니다.
it('rows<=0 이거나, rows 값이 integer가 아닐 경우 error - GET /search?keyword=coffee&rows=<rows>', () => { | ||
const url = (rows: string | number) => `/search?keyword=${keyword}&rows=${rows}`; | ||
const rowsNotAvailables = [-1, -0.1, '0', 'value']; | ||
rowsNotAvailables.forEach((value) => { | ||
request(app.getHttpServer()).get(url(value)).expect(400); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rows도 crossref api에서 한 번에 불러올 수 있는 갯수로 최댓값을 정해두면 좋을 것 같아요.
최댓값 이상을 요청하면 최댓값으로 돌려준다거나 하는 식으로 예외처리 해볼 수 있을 것 같습니다.
describe('/search/paper', () => { | ||
it(`getPaper - doi=10.1234/some_doi 일 때 PaperInfoDetail을 return`, async () => { | ||
const doi = '10.1234/some_doi'; | ||
const paper = await controller.getPaper({ doi }); | ||
expect(paper.references).toBe(5); | ||
expect(paper.referenceList.length).toBe(5); | ||
expect(paper).toBeInstanceOf(PaperInfoDetail); | ||
}); | ||
it('doi가 입력되지 않을 경우 error - GET /search/paper?doi=', () => { | ||
const url = (keyword: string) => `/search/paper?doi=${keyword}`; | ||
request(app.getHttpServer()).get(url('')).expect(400); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
입력받은 doi가 올바르지 않을 때(crossref에 없을 때)의 케이스도 있으면 좋을 것 같습니다! (404 에러)
개요
특정 키워드에 대한
totalPages
가 20일 경우, 사용자가 21page
에 대한 요청을 보내면 NotFoundException(404)를 던져줍니다.작업사항
citations
내림차순 순으로 데이터 전달title
이 항상 있는 데이터만 전달MAIL_TO
환경변수 추가 필요)추가
/search/paper
의 결과로 오는referenceList
의 각 값에key
추가리뷰 요청사항
src/search/tests/search.service.mock.ts
에서 mockup 하는 방식src/search/tests/search.controller.spec.ts
에서 데이터의 type을 검사하는 방식이 방식이 아니면, PaperInfo, PaperInfoExtended, PaperInfoDetail interface들을 이전처럼 class로 돌려서 generic 함수를 만드는 방법을 사용할 수는 있을 것 같습니다.
어떤 방식으로 개선할 수 있을까요?
-> 이 부분은 swagger api를 붙이기 위한 편의성을 위해 class로 선언하겠습니다.